Трехмерные графики функций

В этой главе мы разработаем Windows-приложение, которое в контексте OpenGL изображает трехмерный график функции, заданной произвольным массивом чисел. Данные для графика могут быть прочтены из файла, на который указывает пользователь. Кроме этого, пользователь будет иметь возможность перемещать график вдоль трех пространственных осей, вращать его вокруг вертикальной и горизонтальной осей и просматривать как в обычном, так и скелетном режим. Регулируя параметры освещения поверхности, пользователь может добиться наибольшей реалистичности изображения, то есть усилить визуальный эффект трехмерного пространства на плоском экране.

Графики могут представлять собой результаты расчета какого-либо физического поля, например поверхности равной температуры, давления, скорости, индукции, напряжения и т. д. в части трехмерного пространства, называемой расчетной областью. Пользователь объекта должен заранее подготовить данные и записать их в определенном формате в файл. Объект по команде пользователя считывает данные, нормирует, масштабирует и изображает в своем окне, внедренном в окно приложения-клиента. Пользователь, манипулируя мышью, управляет местоположением и вращением графика, а открыв окно диалога Properties, изменяет другие его атрибуты.

Настройка проекта

  1. На странице VS Home Page выберите команду (гиперссылку) Create New Project.
  2. В окне диалога New Project выберите уже знакомый вам тип проекта: MFC Application, задайте имя проекта OG и нажмите ОК.
  3. В окне мастера MFC Application Wizard выберите вкладку Application Type и задайте такие настройки проекта: Single documents, MFC Standard, Document/View architecture support, Use MFC in a shared DLL.
  4. Перейдите на страницу Advanced Features диалога и снимите флажки Printing and print preview, ActiveX Controls, так как мы не будем использовать эти возможности.
  5. Нажмите кнопку Finish.

Этот тип стартовой заготовки позволяет работать с окном (cocview), которое помещено в клиентскую область окна-рамки (CMainFrame), и создать в этом окне контекст передачи OpenGL. Класс документа нам не понадобится, так как мы собираемся производить все файловые операции самостоятельно, используя свой собственный двоичный формат данных. В связи с этим нам не нужна помощь в сериализации данных, которую предоставляет документ. Для использования функций библиотеки OpenGL надо сообщить компоновщику, чтобы он подключил необходимые библиотеки OpenGL, на сей раз только две.

  1. Поставьте фокус на элемент OG в окне Solution Explorer и дайте Команду Project > Properties (или ее эквивалент View > Property Pages).
  2. В окне открывшегося диалога OG Property Pages выберите элемент дерева Linker > Input.
  3. Переведите фокус в поле Additional Inputs окна справа и добавьте в конец существующего текста имена файлов с описаниями трех библиотек: OPENGL32.LIB GLU32.LIB. Убедитесь в том, что все имена разделены пробелами и нажмите ОК.

Чтобы покончить с настройками общего характера, вставьте в конец файла StdAfx.h строки, которые обеспечивают видимость библиотеки OpenGL, а также некоторых ресурсов библиотеки STL:

#include <afxdlgs.h>

#include <raath.h>

//=== Подключение заголовков библиотек OpenGL

#include <gl/gl.h>

#include <gl/glu.h>

#include <vector>

using namespace std;

Вспомогательный класс

Нам вновь, как и в предыдущем уроке, понадобится класс, инкапсулирующий функциональность точки трехмерного пространства CPoint3D. Контейнер объектов этого класса будет хранить вершины изображаемой поверхности. В коде, который приведен ниже, присутствует слегка измененное по сравнению с предыдущим объявление класса CPoint3D, а также объявления новых данных и методов класса cocview. Заодно мы произвели упрощения стартового кода, которые обсуждались в уроке 5. Весь код введите в файл OGView.h вместо существующей в нем заготовки. Файл должен приобрести следующий вид1:

#pragma once

//========== Вспомогательный класс

class CPointSD

{

public: //====== Координаты точки

float x;

float у;

float z;

//====== Набор конструкторов

CPointSD ()

{

х = у - z = 0.f;

}

CPoint3D (float cl, float c2, float c3)

{

x = cl; z = c2; У = сЗ; ,

}

//====== Операция присвоения

CPoint3DS operator= (const CPointSDS pt)

x = pt.x; z = pt.z;

return *this;

У = pt.y;

//====== Конструктор копирования

CPointSD (const CPoint3D& pt)

{

*this = pt;

//=========== Класс окна OpenGL

class COGView :

public CView

{

protected:

COGView () ;

DECLARE_DYNCREATE(COGView)

public:

virtual ~COGView();

virtual void OnDraw(CDC* pDC) ;

virtual BOOL PreCreateWindow(CREATESTRUCT& cs) ,

//======= Новые данные класса

long m_BkClr; //

int m_LightParara[ll]; //

HGLRC m_hRC; //

HDC m_hdc; //

GLfloat m_AngleX; //

GLfloat m_AngleY; //

GLfloat m_AngleView; //

GLfloat m_fRangeX; //

GLfloat m_fRangeY; //

GLfloat m_fRangeZ; //

GLfloat m_dx; //

GLfloat m_dy; //

GLfloat m_xTrans; //

GLfloat m_yTrans; //

GLfloat m_zTrans; //

GLenura m_FillMode; //

bool m_bCaptured; //

bool m_bRightButton; //

bool m_bQuad; //

CPoint m_pt; //

UINT m_xSize; //

UINT m_zSize; //

//====== Массив вершин поверхности

vector <CPoint3D> m_cPoints;

//====== Новые методы класса

//=-==== Подготовка изображения

void DrawScene();

Цвет фона окна Параметры освещения Контекст OpenGL Контекст Windows Угол поворота вокруг оси X Угол поворота вокруг оси Y Угол перспективы Размер объекта вдоль X Размер объекта вдоль Y Размер объекта вдоль Z Квант смещения вдоль X Квант смещения вдоль Y Смещение вдоль X Смещение вдоль Y Смещение вдоль Z Режим заполнения полигонов Признак захвата мыши Флаг правой кнопки мыши Флаг использования GL_QUAD Текущая позиция мыши Текущий размер окна вдоль X Текущий размер окна вдоль Y

//====== Создание графика по умолчанию

void DefaultGraphic();

//====== Создание массива по данным из буфера

void SetGraphPoints(BYTE* buff, DWORD nSize);

//====== Установка параметров освещения

void SetLight();

//====== Изменение одного из параметров освещения

void SetLightParam (short lp, int nPos);

//====== Определение действующих параметров освещения

void GetLightParams(int *pPos); //====== Работа с файлом данных

void ReadData();

//====== Чтение данных из файла

bool DoRead(HANDLE hFile);

//====== Установка Работа с файлом данных

void SetBkColor();

DECLARE MESSAGE MAP()

Реакции на сообщения Windows

Вспомните, как вы ранее вводили в различные классы реакции на сообщения Windows и повторите эти действия для класса cOGView столько раз, сколько необходимо, чтобы в нем появились стартовые заготовки функций обработки следующих сообщений:

В конструктор класса вставьте код установки начальных значений переменных:

COGView::COGView()

{

//====== Контекст передачи пока отсутствует

m_hRC = 0;

//====== Начальный разворот изображения

m_AngleX = 35.f;

m_AngleY = 20.f;

//====== Угол зрения для матрицы проекции

m_AngleView = 45.f;

//====== Начальный цвет фона

m_BkClr = RGB(0, 0, 96);

// Начальный режим заполнения внутренних точек полигона

m_FillMode = GL_FILL;

//====== Подготовка графика по умолчанию

DefaultGraphic();

//====== Начальное смещение относительно центра сцены

//====== Сдвиг назад на полуторный размер объекта

m_zTrans = -1.5f*m_fRangeX;

m_xTrans = m_yTrans = 0.f;

//== Начальные значения квантов смещения (для анимации)

m_dx = m_dy = 0.f;

//====== Мышь не захвачена

m_bCaptured = false;

//====== Правая кнопка не была нажата

m_bRightButton = false;

//====== Рисуем четырехугольниками

m_bQuad = true;

//====== Начальный значения параметров освещения

m_LightParam[0] = 50; // X position

m_LightParam[l] = 80; // Y position

m_LightParam[2] = 100; // Z position

m_LightParam[3] = 15; // Ambient light

m_LightParam[4] = 70; // Diffuse light

m_LightParam[5] = 100; // Specular light

m_LightParam[6] = 100; // Ambient material

m_LightParam[7] = 100; // Diffuse material

m_LightParam[8] = 40; // Specular material

m_LightParam[9] = 70; // Shininess material

m_LightParam[10] =0; // Emission material

}

Подготовка окна

Вы помните, что подготовку контекста передачи OpenGL надо рассматривать как некий обязательный ритуал, в котором порядок действий определен. В этой процедуре выделяют следующие шаги:

Как было отмечено ранее, окнам, которые в своей клиентской области используют контекст передачи OpenGL, при создании следует задать биты стиля WS_CLIPCHILDREN и ws_CLiPSiBLiNGS. Сделайте это внутри существующего тела функции PreCreateWindow класса cocview, добавив нужные биты стиля к тем, что устанавливаются в заготовке:

BOOL COGView::PreCreateWindow(CREATESTRUCT& cs)

{

//====== Добавляем биты стиля, нужные OpenGL

cs.style |= WS_CLIPSIBLINGS | WS_CLIPCHILDREN;

return CView::PreCreateWindow(cs);

}

Вы помните, что окно OpenGL не должно позволять Windows стирать свой фон, так как данная операция сильно тормозит работу конвейера. В связи с этим введите в функцию обработки WM_ERASEBKGND код, сообщающий системе, что сообщение уже обработано:

BOOL COGView::OnEraseBkgnd(CDC* pDC)

{

return TRUE;

}

Окно OpenGL имеет свой собственный формат пикселов. Нам следует выбрать и установить подходящий формат экранной поверхности в контексте устройства HDC, а затем создать контекст передачи изображения (HGLRC). Для описания формата пикселов экранной поверхности используется структура PIXELFORMATDESCRIPTOR. Выбор формата зависит от возможностей карты и намерений разработчика. Мы зададим в полях этой структуры такие настройки:

В функцию OnCreate введите код подготовки окна OpenGL. Работа здесь ведется со структурой PIXELFORMATDESCRIPTOR. Кроме того, в ней создается контекст m_hRC и устанавливается в качестве текущего:

int COGView::OnCreate(LPCREATESTROCT IpCreateStruct)

{

if (CView::OnCreate(IpCreateStruct) == -1)

return -1;

PIXELFORMATDESCRIPTOR pfd = // Описатель формата

{

sizeof(PIXELFORMATDESCRIPTOR), // Размер структуры

1, // Номер версии

PFD_DRAW_TO_WINDOW | // Поддержка GDI

PFD_SUPPORT_OPENGL | // Поддержка OpenGL

PFD_DOUBLEBUFFER, // Двойная буферизация

PFD_TYPE_RGBA, // Формат RGBA, не палитра

24, // Количество плоскостей

//в каждом буфере цвета

24, 0, // Для компонента Red

24, 0, // Для компонента Green

24, 0, // Для компонента Blue

24, 0, // Для компонента Alpha

0, // Количество плоскостей

// буфера Accumulation

0, // То же для компонента Red

0, // для компонента Green

0, // для компонента Blue

0, // для компонента Alpha

32, // Глубина 2-буфера

0, // Глубина буфера Stencil

0, // Глубина буфера Auxiliary

0, // Теперь игнорируется

0, // Количество плоскостей

0, // Теперь игнорируется

0, // Цвет прозрачной маски

0 // Теперь игнорируется };

//====== Добываем дежурный контекст

m_hdc = ::GetDC(GetSafeHwnd());

//====== Просим выбрать ближайший совместимый формат

int iD = ChoosePixelForraat(m_hdc, spfd);

if ( !iD )

{

MessageBoxC'ChoosePixelFormat: :Error") ;

return -1;

}

//====== Пытаемся установить этот формат

if ( ISetPixelFormat (m_hdc, iD, Spfd) )

{

MessageBox("SetPixelFormat::Error");

return -1;

}

//====== Пытаемся создать контекст передачи OpenGL

if ( !(m_hRC = wglCreateContext (m_hdc)))

{

MessageBox("wglCreateContext::Error");

return -1;

}

//====== Пытаемся выбрать его в качестве текущего

if ( IwglMakeCurrent (m_hdc, m_hRC))

{

MessageBox("wglMakeCurrent::Error");

return -1;

//====== Теперь можно посылать команды OpenGL

glEnable(GL_LIGHTING); // Будет освещение

//====== Будет только один источник света

glEnable(GL_LIGHTO);

//====== Необходимо учитывать глубину (ось Z)

glEnable(GL_DEPTH_TEST);

//====== Необходимо учитывать цвет материала поверхности

glEnable(GL_COLOR_MATERIAL);

//====== Устанавливаем цвет фона .

SetBkColor () ;

//====== Создаем изображение и запоминаем в списке

DrawScene () ;

return 0;

}

Контекст передачи (rendering context) создается функцией wglCreateContext с учетом выбранного формата пикселов. Так осуществляется связь OpenGL с Windows. Создание контекста требует, чтобы обычный контекст существовал и был явно указан в параметре wglCreateContext. HGLRC использует тот же формат пикселов, что и НОС. Мы должны объявить контекст передачи в качестве текущего (current) и лишь после этого можем делать вызовы команд OpenGL, которые производят включение некоторых тумблеров в машине состояний OpenGL. Вызов функции DrawScene, создающей и запоминающей изображение, завершает обработку сообщения. Таким образом, сцена рассчитывается до того, как приходит сообщение о перерисовке WM_PAINT. Удалять контекст передачи надо после отсоединения его от потока. Это делается в момент, когда закрывается окно представления. Введите в тело заготовки OnDestroy следующие коды:

void COGView::OnDestroy(void)

{

//====== Останавливаем таймер анимации

KillTimer(1);

//====== Отсоединяем контекст от потока

wglMakeCurrent(0, 0); //====== Удаляем контекст

if (m_hRC)

{

wglDeleteContext(m_hRC);

m_hRC = 0;

}

CView::OnDestroy() ;

}

Так же как и в консольном проекте OpenGL, обработчик сообщения WM_SIZE должен заниматься установкой прямоугольника просмотра (giviewport) и мы, так же как и раньше, зададим его равным всей клиентской области окна. -Напомним, что конвейер OpenGL использует эту установку для того, чтобы поместить изображение в центр окна и растянуть или сжать его пропорционально размерам окна. Кроме того, в обработке onSize с помощью матрицы проецирования (GL_PROJECTION) задается тип проекции трехмерного изображения на плоское окно. Мы выбираем центральный или перспективный тип проецирования и задаем при этом угол зрения равным m_AngleView. В конструкторе ему было присвоено значение в 45 градусов:

void COGView::OnSize(UINT nType, int ex, int cy)

{

//====== Вызов родительской версии

CView::OnSize(nType, ex, cy) ;

//====== Вычисление диспропорций окна

double dAspect = cx<=cy ? double(cy)/ex : double(ex)/cy;

glMatrixMode (GL_PROJECTION) ;

glLoadldentity() ;

//====== Установка режима перспективной проекции

gluPerspective (m_AngleView, dAspect, 0.01, 10000.);

//====== Установка прямоугольника просмотра

glViewport(0, 0, сх, су);
}

Реакция на сообщение о перерисовке

В функции перерисовки должна выполняться стандартная последовательность действий, которая стирает back-буфер и буфер глубины, корректирует матрицу моделирования, вызывает из списка команды рисования и по завершении рисования переключает передний и задний буферы. Полностью замените существующий текст функции OnDraw на тот, который приведен ниже: void COGView:: OnDtaw (CDC" pDC]

glClear<GL_COLOft_BUFFER_BIT | GL_BEPTH_3UFFER_BIT);

glMatrixMode(GLjtoDELVIEH) ;

glLoadldentitylT;

SetLight() ;

//=====Формировать

//===== Переключение буферов SwapBuffera

}

Параметры освещения

Установка параметрпв освещения осуществляется подобно тому, как это делалось в предыдущем уроке. Но здесь мы храним все параметры для тога, чтобы можно было управлять освещенностью изображения. Немного позже разработаем диалог, с помощью которого пользователь программы сможет изменять настройки освещения, а сейчас введите коды функции SetLight:

void COGView::SetLight()

{

//====== Обе поверхности изображения участвуют

//====== при вычислении цвета пикселов

//====== при учете параметров освещения

glLightModeli(GL_LIGHT_MODEL_TWO_SIDE, 1);

//====== Позиция источника освещения

//====== зависит от размеров объекта

float fPos[] =

{

(m_LightParam[0]-50)*m_fRangeX/100,

(m_LightParam[l]-50)*m_fRangeY/100,

(m_LightParam[2]-50)*m_fRangeZ/100,

l.f

};

glLightfv(GL_LIGHTO, GL_POSITION, fPos);

/1 ====== Интенсивность окружающего освещения

float f = m_LightParam[3]/100.f;

float fAmbient[4] = { f, f, f, O.f };

glLightfv(GL_LIGHTO, GL_AMBIENT, fAmbient);

//====== Интенсивность рассеянного света

f = m_LightParam[4]/100.f;

float fDiffuse[4] = { f, f, f, O.f };

glLightfv(GL_LIGHTO, GL_DIFFUSE, fDiffuse);

//====== Интенсивность отраженного света

f = m_LightParam[5]/100.f;

float fSpecular[4] = { f, f, f, 0.f };

glLightfv(GL_LIGHTO, GL_SPECULAR, fSpecular);

//====== Отражающие свойства материала

//====== для разных компонентов света

f = m_LightParam[6]/100.f;

float fAmbMat[4] = { f, f, f, 0.f };

glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT, fAmbMat);

f = m_LightParam[7]/100.f;

float fDifMat[4] = { f, f, f, 1.f };

glMaterialfv(GL_FRONT_AND_BACK, GL_DIFFUSE, fDifMat);

f = m_LightParam[8]/100.f;

float fSpecMat[4] = { f, f, f, 0.f };

glMaterialfv(GL_FRONT_AND_BACK, GL_SPECULAR, fSpecMat);

//====== Блесткость материала

float fShine = 128 * m_LightParam[9]/100.f;

glMaterialf(GL FRONT AND BACK, GL SHININESS, fShine);

//====== Излучение света материалом

f = m_LightParam[10]/100.f;

float f Emission [4] = { f , f , f , 0 . f } ;

glMaterialfv(GL_FRONT_AND_BACK, GL_EMISSION, fEmission) ;

}

Установка цвета фона

Введите вспомогательную функцию, которая позволяет вычислить и изменить цвет фона окна OpenGL. Позже мы введем возможность выбора цвета фона с помощью стандартного диалога Windows по выбору цвета:

void COGView: :SetBkColor ()

{

//====== Расщепление цвета на три компонента

GLclampf red = GetRValue (m_BkClr) /255 . f ,

green = GetGValue (m_BkClr) /255. f ,

blue = GetBValue(m_BkClr) /255. f ;

//====== Установка цвета фона (стирания) окна

glClearColor (red, green, blue, 0.f);

//====== Непосредственное стирание

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) ;

}

Подготовка изображения

Разработаем код функции DrawScene, которая готовит и запоминает изображение на основе координат вершин, хранимых в контейнере m_cPoints. Изображение по выбору пользователя формируется либо в виде криволинейных четырехугольников (GL_QUADS), либо в виде полосы связанных четырехугольников (GL_QUAD_STRIP). Точки изображаемой поверхности расположены над регулярной координатной сеткой узлов в плоскости (X, Z). Размерность этой сетки хранится в переменных m_xSize и m_zSize. Несмотря на двухмерный характер сетки, для хранения координат вершин мы используем линейный (одномерный) контейнер m_cPoints, так как это существенно упрощает объявление контейнера и работу с ним. В частности, упрощаются файловые операции. Выбор четырех смежных точек генерируемого примитива (например, GL_QUADS) происходит с помощью четырех индексов (n, i, j, k). Индекс п последовательно пробегает по всем вершинам в порядке слева направо. Более точно алгоритм перебора вершин можно определить так: сначала проходим по сетке узлов вдоль оси X при Z = 0, затем увеличиваем Z и вновь проходим вдоль X и т. д. Индексы i, j, k вычисляются относительно индекса п. В ветви связанных четырехугольников (GL_QUAD_STRIP) работают только два индекса.

В контейнер m_cPoints данные попадают после того, как они будут прочитаны из файла. Для того чтобы при открытии приложения в его окне уже находился график функции, необходимо заранее создать файл с данными по умолчанию, открыть и прочесть его содержимое. Это будет сделано в коде функций

DefaultGraphic и SetGraphPoints. Алгоритм функции DrawScene разработан в предположении, что контейнер точек изображаемой поверхности уже существует. Флаг m_bQuad используется для выбора способа создания полигонов: в виде отдельных (GL_QUADS) или связанных (GL_QUAD_STRIP) четырехугольников. Позднее мы введем команду меню для управления этой регулировкой:

void COGView: : DrawScene ()

{

//====== Создание списка рисующих команд

glNewList (1, GL_COMPILE) ;

//====== Установка режима заполнения

//====== внутренних точек полигонов

glPolygonMode (GL_FRONT_AND_BACK, m_FillMode) ;

//====== Размеры изображаемого объекта

UINTnx = m_xSize-l, nz = m_zSize-l;

//====== Выбор способа создания полигонов

if (m_bQuad)

glBegin (GL_QUADS) ;

//====== Цикл прохода по слоям изображения (ось Z)

for (UINT z=0, i=0; z<nz; z++)

//====== Связанные полигоны начинаются

//====== на каждой полосе вновь

if (!m_bQuad)

glBegin (GLJ2UAD_STRIP) ;

//====== Цикл прохода вдоль оси X

for (UINT x=0; x<nx; x++)

// i, j, k, n — 4 индекса вершин примитива при

// обходе в направлении против часовой стрелки

int j = i + m_xSize, // Индекс узла с большим Z

k = j+1/ // Индекс узла по диагонали

n = i+1; // Индекс узла справа

//=== Выбор координат 4-х вершин из контейнера

float

xi = m_cPoints [i] .x,

yi = m_cPoints [i] .у,

zi = m_cPoints [i] . z,

xj = m_cPoints [ j ] .x,

yj = m_cPoints [ j ] .y,

zj = m_cPoints [ j ] . z,

xk = m_cPoints [k] .x,

yk = m_cPoints [k] .y,

zk = m cPoints [k] . z,

xn = m_cPoints [n] .x,

yn = m_cPoints [n] .y,

zn = m_cPoints [n] . z,

//=== Координаты векторов боковых сторон ах = xi-xn, ay = yi-yn,

by = yj-yi, bz = zj-zi,

//====== Вычисление вектора нормали

vx = ay*bz, vy = -bz*ax, vz = ax*by,

//====== Модуль нормали

v = float (sqrt (vx*vx + vy*vy + vz*vz) ) ;

//====== Нормировка вектора нормали

vx /= v; vy /= v; vz /= v;

//====== Задание вектора нормали

glNorma!3f (vx,vy,vz);

// Ветвь создания несвязанных четырехугольников

if (m_bQuad)

{

//==== Обход вершин осуществляется

//==== в направлении против часовой стрелки

glColorSf (0.2f, 0.8f, l.f);

glVertexSf (xi, yi, zi) ;

glColor3f (0.6f, 0.7f, l.f);

glVertexSf (xj, у j , zj);

glColorSf (0.7f, 0.9f, l.f);

glVertexSf (xk, yk, zk) ;

glColor3f (0.7f, 0.8f, l.f);

glVertexSf (xn, yn, zn) ;

}

else

//==== Ветвь создания цепочки четырехугольников

{

glColor3f (0.9f, 0.9f, l.0f);

glVertexSf (xn, yn, zn) ;

glColorSf (0.5f, 0.8f, l.0f);

glVertexSf (xj, у j , zj);

//====== Закрываем блок команд GL_QUAD_STRIP

if (!m_bQuad) glEnd ( ) ; } //====== Закрываем блок команд GL_QUADS

if (m_bQuad)

glEnd() ;

// ====== Закрываем список команд OpenGL

glEndList() ;

}

При анализе кода обратите внимание на тот факт, что вектор нормали вычисляется по упрощенной формуле, так как линии сетки узлов, над которой расположены вершины поверхности, параллельны осям координат (X, Z). В связи с этим равны нулю компоненты az и bх векторов боковых сторон в формуле для нормали (см. раздел «Точное вычисление нормалей» в предыдущем уроке).

График по умолчанию

Пришла пора создать тестовую поверхность у = f (x, z), которую мы будем демонстрировать по умолчанию, то есть до того, как пользователь обратился к файловому диалогу и выбрал файл с данными, которые он хочет отобразить в окне OpenGL Функция Def aultGraphic, коды которой вы должны вставить в файл ChildView,cpp, задает поверхность, описываемую уравнением:


Yi,j=[3*п*(i-Nz/2)/2*Nz]*SIN
[3*п*(j-Nx/2)/2*Nx]

Здесь п. обозначает количество ячеек сетки вдоль оси Z, а пхколичество ячеек вдоль оси X. Индексы i (0 < i < пz) и j (0 < j < nx) выполняют роль дискретных значений координат (Z, X) и обозначают местоположение текущей ячейки при пробеге по всем ячейкам сетки в порядке, описанном выше. Остальные константы подобраны экспериментально так, чтобы видеть полтора периода изменения гармонической функции.

Мы собираемся работать с двоичным файлом и хранить в нем информацию в своем формате. Формат опишем словесно: сначала следуют два целых числа m_xsize и m_zSize (размеры сетки), затем последовательность значений функции у = f (х, z) в том же порядке, в котором они были созданы. Перед тем как записать данные в файл, мы поместим их в буфер, то есть временный массив buff, каждый элемент которого имеет тип BYTE, то есть unsigned char. В буфер попадают значения переменных разных типов, что немного усложняет кодирование, но зато упрощает процесс записи и чтения, который может быть выполнен одной командой, так как мы пишем и читаем сразу весь буфер. В процессе размещения данных в буфер используются указатели разных типов, а также преобразование их типов:

void COGView::DefaultGraphic()

{

//====== Размеры сетки узлов

m xSize = m zSize = 33;

//====Число ячеек на единицу меньше числа узлов

UINTnz = m_zSize - 1, nx = m_xSize - 1;

// Размер файла в байтах для хранения значений функции

DWORD nSize = m_xSize * m_zSize * sizeof (float) + 2*sizeof (UINT) ;

//====== Временный буфер для хранения данных

BYTE *buff = new BYTE[nSize+l] ;

//====== Показываем на него указателем целого типа

UINT *p = (UINT*)buff;

//====== Размещаем данные целого типа

*р++ = m_xSize;

*р++ = m_zSize;

//====== Меняем тип указателя, так как дальше

//====== собираемся записывать вещественные числа

float *pf = (float*)?;

//=== Предварительно вычисляем коэффициенты уравнения

double fi = atan(l.)*6,

kx = fi/nx,

kz = fi/nz;

//====== В двойном цикле пробега по сетке узлов

//=== вычисляем и помещаем в буфер данные типа float

for (UINT i=0; i<ra_zSize;

for (UINT j=0; j<m_xSize;

{

*pf++ = float (sin(kz* (i-nz/2.) ) * sin (kx* (j-nx/2. ) )

}

}

//=== Переменная для того, чтобы узнать сколько

//=== байт было реально записано в файл DWORD nBytes;

//=== Создание и открытие файла данных sin.dat

HANDLE hFile = CreateFile (_T ("sin .dat") , GENERIC_WRITE,

0, 0, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, 0)

//====== Запись в файл всего буфера

WriteFile (hFile, (LPCVOID) buff, nSize, SnBytes, 0) ;

//====== Закрываем файл

CloseHandle (hFile) ;

//====== Создание динамического массива m_cPoints

SetGraphPoints (buff, nSize) ;

//====== Освобождаем временный буфер

delete [] buff;

}

В процессе создания, открытия и записи в файл мы пользуемся API-функциями CreateFile, WriteFile и CloseHandle, которые предоставляют значительно больше возможностей управлять файловых хозяйством, чем, например, методы класса CFile или функции из библиотек stdio.h или iostream.h. Обратитесь к документации, для того чтобы получить представление о них.

Работа с контейнером

Для работы с файлом мы пользовались буфером переменных типа BYTE. Для работы с данными в памяти значительно более удобной структурой данных является динамический контейнер. Мы, как вы помните, выбрали для этой цели контейнер, скроенный по шаблону vector. При заказе на его изготовление указали тип данных для хранения в контейнере. Это объекты класса CPointSD (точки трехмерного пространства). Мы пошли по простому пути и храним в файле только один компонент Y из трех координат точек поверхности в 3D. Остальные две координаты (узлов сетки на плоскости X-Z) будем генерировать на регулярной основе. Такой подход оправдан тем, что изображение OpenGL все равно претерпевает нормирующие преобразования, перед тем как попасть на двухмерный экран. Создание контейнера точек производится в теле функции SetGraphPoints, к разработке которой сейчас и приступим.

На вход функции подается временный буфер (и его размер), в который попали данные из файла. В настоящий момент в буфере находятся данные тестовой поверхности, а потом, при вызове из функции ReadData, в него действительно попадут данные из файла. Выбор данных из буфера происходит аналогично их записи. Здесь мы пользуемся адресной арифметикой, определяемой типом указателя. Так, операция ++ в применении к указателю типа UINT сдвигает его в памяти на sizeof (UINT) байт. Смена типа указателя (на float*) происходит в тот момент, когда выбраны данные о размерах сетки узлов.

Для надежности сначала проверяем данные из буфера на внутреннюю непротиворечивость в смысле размерностей. Затем мы уничтожаем данные контейнера и генерируем новые на основе содержимого буфера. В процессе генерации трехмерных координат точек их ординаты (Y) масштабируются для того, чтобы график имел пропорции, удобные для просмотра:

void COGView::SetGraphPoints(BYTE* buff, DWORD nSize)

{

//====== Готовимся к расшифровке данных буфера

//====== Указываем на него указателем целого типа

UINT *p = (UINT*)buff;

//=== Выбираем данные целого типа, сдвигая указатель

m_xSize = *р; m_zSize = *++p;

//====== Проверка на непротиворечивость

if (m_xSize<2 || m_zSize<2 ||

m_xSize*m_zSize*sizeof(float)

+ 2 * sizeof(UINT) != nSize)

{

MessageBox (_T ("Данные противоречивы") ) ;

return;

}

//====== Изменяем размер контейнера

//====== При этом его данные разрушаются

m_cPoints . resize (m_xSize*m_zSize) ;

if (m_cPoints .empty () )

{

MessageBox (_T ("He возможно разместить данные")

return;

}

//====== Подготовка к циклу пробега по буферу

//====== и процессу масштабирования

float x, z,

//====== Считываем первую ординату

*pf = (float*) ++р,

fMinY = *pf,

fMaxY = *pf,

right = (m_xSize-l) /2 . f ,

left = -right,

read = (m_zSize-l) /2 . f ,

front = -rear,

range = (right + rear) /2. f;

UINTi, j, n;

//====== Вычисление размаха изображаемого объекта

m_fRangeY = range;

m_fRangeX = float (m_xSize) ;

m_fRangeZ = float (m_zSize) ;

//====== Величина сдвига вдоль оси Z

m_zTrans = -1.5f * m_fRangeZ;

//====== Генерируем координаты сетки (X-Z)

//====== и совмещаем с ординатами Y из буфера

for (z=front, i=0, n=0; i<m_zSize; i++, z+=l.f)

{

for (x=left, j=0; j<m_xSize; j++, x+=l.f, n++)

{

MinMax (*pf, fMinY, fMaxY) ;

m_cPoints[n] = CPoint3D(x, z, *pf++) ;

}

}

//====== Масштабирование ординат

float zoom = fMaxY > fMinY ? range/ (fMaxY-fMinY)

: l.f;

for (n=0; n<m_xSize*m_zSize;n++)

{

m_cPoints [n] . у = zoom * (m_cPoints [n] . у - fMinY) - range/2. f;

}

}

При изменении размеров контейнера методом (resize) все его данные разрушаются. В двойном цикле пробега по узлам сетки мы восстанавливаем (генерируем заново) координаты X и Z всех вершин четырехугольников. В отдельном цикле пробега по всему контейнеру происходит масштабирование ординат (умножение на предварительно вычисленный коэффициент zoom). В используемом алгоритме необходимо искать экстремумы функции у = f (x, z). С этой целью удобно иметь глобальную функцию MinMax, которая корректирует значение минимума или максимума, если входной параметр превышает существующие на сей момент экстремумы. Введите тело этой функции в начало файла реализации оконного класса (ChildView.cpp):

inline void MinMax (float d, floats Min, float& Max)

{

//====== Корректируем переданные по ссылке параметры

if (d > Max)

Max = d; // Претендент на максимум

else if (d < Min)

Min = d; // Претендент на минимум

}

Чтение данных

В теле следующей функции ReadData мы создадим файловый диалог, в контексте которого пользователь выбирает файл с новыми данными графика, затем вызовем функцию непосредственного чтения данных (DoRead) и создадим новую сцену на основе прочитанных данных. Попутно мы демонстрируем, как обрабатывать ошибки и работать с файловым диалогом, созданным с помощью функций API. Стандартный диалог открытия файла в этом случае более управляем, и ему можно придать множество сравнительно новых стилей. Стиль OFN_EXPLORER работает только в Windows 2000:

void COGView: : ReadData ()

{

//=== Строка, в которую будет помещен файловый путь

TCHAR szFile[MAX_PATH] = { 0 } ;

//====== Строка фильтров демонстрации файлов

TCHAR *szFilter =TEXT ("Graphics Files (*.dat)\0")

TEXT("*.dat\0")

TEXT ("All FilesNO")

TEXT ( " * . * \ 0 " ) ;

//====== Выявляем текущую директорию

TCHAR szCurDir[MAX_PATH] ;

: :GetCurrentDirectory (MAX_PATH-1, szCurDir) ;

//== Структура данных, используемая файловым диалогом

OPENFILENAME ofn;

ZeroMemory (&ofn,sizeof (OPENFILENAME) ) ;

//====== Установка параметров будущего диалога

ofn.lStructSize = sizeof (OPENFILENAME) ;

и * . *, текстовые описания которых можно увидеть в одном из окон стандартного диалога поиска и открытия файлов:

//====== Функция непосредственного чтения данных

bool COGView: : DoRead ( HANDLE hFile) {

//====== Сначала узнаем размер файла

DWORD nSize = GetFileSize (hFile, 0) ;

//=== Если не удалось определить размер, GetFileSize

//====== возвращает 0xFFFFFFFF

if (nSize == 0xFFFFFFFF)

{

GetLastError () ;

MessageBox (_T ("Некорректный размер файла"));

CloseHandle (hFile) ;

return false;

//=== Создаем временный буфер размером в весь файл BYTE

*buff = new BYTE [nSize+1] ;

//====== Обработка отказа выделить память

if (Ibuff) {

MessageBox (_T ("Слишком большой размер файла"))

CloseHandle (hFile) ;

return false;

//====== Реальный размер файла

DWORD nBytes;

//====== Попытка прочесть файл

ReadFile (hFile, buff, nSize, &nBytes, 0) ; CloseHandle (hFile) ;

//====== Если реально прочитано меньшее число байт

if (nSize != nBytes)

{

MessageBox (_T ("Ошибка при чтении файла"));

return false;

}

//====== Генерация точек изображения

SetGraphPoints (buff, nSize) ;

//====== Освобождение временного буфера

delete [] buff;

// ====== Возвращаем успех

return true;

}

В данный момент можно запустить приложение, и оно должно работать. В окне вы должны увидеть изображение поверхности, которое приведено на рис. 7.1. Для создания рисунка мы изменили цвет фона на белый, так как в книге этот вариант считается более предпочтительным. Попробуйте изменить размеры окна. Изображение поверхности должно пропорционально изменить свои размеры. Оцените качество интерполяции цветов внутренних точек примитивов и степень влияния освещения. Позже мы создадим диалог для управления параметрами света и отражающих свойств материала. А сейчас отметим, что напрашивается введение возможности управлять ориентацией и местоположением поверхности с помощью мыши. Для того чтобы убедиться в сложности автомата состояний OpenGL, a также в том, что все в нем взаимосвязано, временно поменяйте местами две строки программы: glVertexSf (xi, yi, zi); и glVertex3f (xn, yn, zn);. Вы найдете их в теле функции DrawScene.

Рис. 7.1. Вид освещенной поверхности в 3D

Управление изображением с помощью мыши

Итак, мы собираемся управлять ориентацией изображения с помощью левой кнопки мыши. Перемещение курсора мыши при нажатой кнопке должно вращать изображение наиболее естественным образом, то есть горизонтальное перемещение должно происходить вокруг вертикальной оси Y, а вертикальное — вокруг горизонтальной оси X. Если одновременно с мышью нажата клавиша Ctrl, то мы будем перемещать (транслировать) изображение вдоль осей X и Y. С помощью правой кнопки будем перемещать изображение вдоль оси Z. Кроме того, с помощью левой кнопки мыши мы дадим возможность придать вращению постоянный характер. Для этого в обработчик WM_LBUTTONUP введем анализ на превышение квантом перемещения (m_dx, m_dy) некоторого порога чувствительности. Если он превышен, то мы запустим таймер, и дальнейшее вращение будем производить с его помощью. Если очередной квант перемещения ниже порога чувствительности, то мы остановим таймер, прекращая вращение. В обработке WM_MOUSEMOVE следует оценивать желаемую скорость вращения, которая является векторной величиной из двух компонентов и должна быть пропорциональна разности двух последовательных координат курсора. Такой алгоритм обеспечивает гибкое и довольно естественное управление ориентацией объекта. Начнем с обработки нажатия левой кнопки. Оно, очевидно, должно всегда останавливать таймер, запоминать факт нажатия кнопки и текущие координаты курсора мыши:

void COGView: :OnLButtonDown (UINT nFlags, CPoint point)

{

//====== Останавливаем таймер

KillTimer(1);

//====== Обнуляем кванты перемещения

m_dx = 0.f; m_dy = 0.f;

//====== Захватываем сообщения мыши,

//====== направляя их в свое окно

SetCapture ();

//====== Запоминаем факт захвата

m_bCaptured = true;

//====== Запоминаем координаты курсора

m_pt = point;

}

При нажатии на правую кнопку необходимо выполнить те же действия, что и при нажатии на левую, но дополнительно надо запомнить сам факт нажатия правой кнопки, с тем чтобы правильно интерпретировать последующие сообщения о перемещении указателя мыши и вместо вращения производить сдвиг вдоль оси Z:

void COGView::OnRButtonDown(UINT nFlags, CPoint point)

{

//====== Запоминаем факт нажатия правой кнопки

m_bRightButton = true;

//====== Воспроизводим реакцию на левую кнопку

OnLButtonDown(nFlags, point);

}

В обработчик отпускания левой кнопки мы вводим анализ на необходимость продолжения вращения с помощью таймера. В случае превышения порога чувствительности, запускаем таймер, сообщения от которого будут говорить, что надо продолжать вращение, поддерживая текущее значение скорости:

void COGView::OnLButtonUp(UINT nFlags, CPoint point)

{

//====== Если был захват,

if (m_bCaptured)

//=== то анализируем желаемый квант перемещения

//=== на превышение порога чувствительности

if (fabs(m_dx) > 0.5f || fabs(m_dy) > 0.5f)

//=== Включаем режим постоянного вращения

SetTimer(1,33,0);

else

//=== Выключаем режим постоянного вращения

KillTimer(1);

//====== Снимаем флаг захвата мыши

m_bCaptured = false;

//====== Отпускаем сообщения мыши

ReleaseCapture();

}

}

Отпускание правой кнопки должно просто отмечать факт прекращения перемещения вдоль оси Z и отпускать сообщения мыши для того, чтобы они работали на другие окна, в том числе и на наше окно-рамку. Если этого не сделать, то станет невозможным использование меню главного окна. Проверьте, если хотите. Для этого достаточно закомментировать вызов функции ReleaseCapture в обеих функциях:

void COGView::OnRButtonUp(UINT nFlags, CPoint point)

{

//====== Правая кнопка отпущена

m_bRightButton = false;

//====== Снимаем флаг захвата мыши

m_bCaptured = false;

//====== Отпускаем сообщения мыши

ReleaseCapture();

}

Теперь реализуем самую сложную часть алгоритма — реакцию на перемещение курсора. Здесь мы должны оценить желаемую скорость вращения. Она зависит от того, насколько резко пользователь подвинул объект, то есть оценить модуль разности двух последних позиций курсора, В этой же функции надо выделить случай одновременного нажатия служебной клавиши Ctrl Если она нажата, то интерпретация движения мыши при нажатой левой кнопке изменяется. Теперь вместо вращения мы должны сдвигать объект, то есть пропорционально изменять переменные m_xTrans и m_yTrans, которые затем подаются на вход функции glTranslate. Третья ветвь алгоритма обрабатывает движение указателя при нажатой правой кнопке. Здесь необходимо изменять значение переменной m_zTrans, обеспечивая сдвиг объекта вдоль оси Z. Числовые коэффициенты пропорциональности, которые вы видите в коде функции, влияют на чувствительность мыши и подбираются экспериментально. Вы можете изменить их на свой вкус так, чтобы добиться желаемой управляемости изображения:

void COGView::OnMouseMove(UINT nFlags, CPoint point)

{

if (m_bCaptured) // Если был захват,

{

// Вычисляем компоненты желаемой скорости вращения

m_dy = float (point .у - m_pt .у) /40 . f ;

m_dx = float (point .x - m_pt .x) /40. f ;

//====== Если одновременно была нажата Ctrl,

if (nFlags & MK_CONTROL)

{

//=== Изменяем коэффициенты сдвига изображения

m_xTrans += m_dx;

m_yTrans -= m_dy;

}

else

{

//====== Если была нажата правая кнопка

if (m_bRightButton)

//====== Усредняем величину сдвига

m_zTrans += (m_dx + m_dy)/2.f;

else

{

//====== Иначе, изменяем углы поворота

m_AngleX += m_dy;

m_AngleY += m_dx;

}

}

//=== В любом случае запоминаем новое положение мыши

m_pt = point; Invalidate (FALSE) ;

}

}

Запустите и проверьте управляемость объекта. Введите коррективы чувствительности на свой вкус. Попробуйте скорректировать эффект влияния поворота вокруг оси X на интерпретацию знака желаемого вращения вокруг оси Y. Здесь можно воспользоваться стеком матриц моделирования. Теперь добавим код в заготовку функции реакции на сообщения таймера с тем, чтобы ввести фиксацию состояния вращения.

Включаем анимацию

Реакция на сообщение о том, что истек очередной квант времени в 33 миллисекунды (именно такую установку мы сделали в OnLButtonUp) выглядит очень просто. Увеличиваем углы поворота изображения на те кванты, которые вычислили в функции OnMouseMove и вызываем перерисовку окна. Так как при непрерывном вращении углы постоянно растут, то можно искусственно реализовать естественную их периодичность с циклом в 360 градусов. Однако с этой задачей успешно справляется OpenGL, и вы можете убрать код ограничения углов:

void COGView: :OnTimer (UINT nIDEvent)

{

//====== Если это был наш таймер

if (nIDEvent==l)

{

//====== Увеличиваем углы поворота

m_AngleX += m_dy;

m_AngleY += m_dx;

//====== Ограничители роста углов

if (m_AngleX > 360)

m_AngleX -= 360;

if (m_AngleX <-360)

m_AngleX += 360;

if (m_AngleY > 360)

m_AngleY -=360;

if (m_AngleY <-360)

m_AngleY +=360;

//====== Просим перерисовать окно

Invalidate(FALSE);

}

else

//=== Каркас приложения обработает другие таймеры

CView::OnTimer(nIDEvent);

}

Запустите и протестируйте приложение. Скорректируйте, если необходимо, коэффициенты чувствительности.

Ввод новых команд

Вы заметили, что до сих пор обходились без каких-либо ресурсов. Мы не учитываем традиционный диалог About, планку меню главного окна, панель инструментов, две таблицы (строк и ускорителей) и два значка, которые присутствовали в каркасе приложения изначально. Дальнейшее развитие потребует ввести новые ресурсы. Главным из них будет диалог, который мы запустим в немодальном режиме и который позволит подробно исследовать влияние параметров освещения на качество изображения. Начинать, как обычно, следует с команд меню. Скорректируйте меню главного окна так, чтобы в нем появились новые команды:

Одновременно удалите не используемые нами команды: File > New, File > Open, File > Save, File > Save as, File > Recent File, Edit > Undo, Edit > Cut, Edit > Copy и Edit > Paste.

Вы, конечно, знаете, что идентификаторы команд можно не задавать. Они генерируются автоматически при перемещении фокуса от вновь созданной команды к любой другой.

После этого в классе cocview создайте обработчики всех новых команд с именами по умолчанию (их предлагает Studio.Net). При создании реакций на эти команды меню (COGView > Properties > Events) предварительно раскройте все необходимые элементы в дереве Properties t Commands. Одновременно с функциями обработки типа COMMAND создайте (для всех команд, кроме Edit > Background) функции обновления пользовательского интерфейса, то есть функции обработки типа UPDATE_ COMMANDJJI. Они, как вы помните, следят за состоянием команд меню и соответствующих им кнопок панели управления, обновляя интерфейс пользователя. Команды становятся доступными или, наоборот, в зависимости признака, управляемого програмистом.

В обработчике OnEditBackground мы вызовем стандартный диалог по выбору цвета, сразу открыв обе его страницы (см. флаг CC_FULLOPEN). С помощью этого диалога пользователь сможет изменить цвет фона:

void COGView::OnEditBackground(void)

{

//====== Создаем объект диалогового класса

CColorDialog dig(m_BkClr); //====== Устанавливаем бит стиля

dig.m_cc.Flags |= CC_FULLOPEN;

//====== Запускаем диалог и выбираем результат

if (cilg.DoModal ()==IDOK)

{

m_BkClr = dig.m_cc.rgbResuit;

//====== Изменяем цвет фона

SetBkColor();

Invalidate(FALSE);

}

}

Проверьте результат, запустив приложение и вызвав диалог. При желании создайте глобальный массив с 16 любимыми цветами и присвойте его адрес переменной lpCustColors, которая входит в состав полей структуры m_сс, являющейся членом класса CColorDialog. В этом случае пользователь сможет подобрать и запомнить некоторые цвета.

В обработчик OnViewQuad введите коды, инвертирующие булевский признак m_bQuad, который мы используем как флаг необходимости рисования отдельными четырехугольниками (GL_QUADS), и заново создают изображение. Если признак инвертирован, то мы рисуем полосами (GL_QUAD_STRIP):

void COGView::OnViewQuad(void)

{

// Инвертируем признак стиля задания четырехугольников

m_bQuad = ! m_bQuad;

//====== Заново создаем изображение

DrawScene (); Invalidate(FALSE); UpdateWindow();

}

В обработчик команды обновления интерфейса введите коды, которые обеспечивают появление маркера выбора рядом с командой меню (или залипания кнопки панели управления):

void COGView::OnUpdateViewQuad(CCmdUI* pCmdUI)

{

//====== Вставляем или убираем маркер (пометку)

pCmdUI->SetCheck(m_bQuad==true);

}

Проверьте результат и попробуйте объяснить зубчатые края поверхности (рис. 7.2). Не знаю, правильно ли я поступаю, когда по ходу изложения вставляю задачи подобного рода. Но мной движет желание немного приоткрыть дверь в кухню разработчика и показать, что все не так уж просто. Искать ошибки в алгоритме, особенно чужом, является очень кропотливым занятием. Однако совершенно необходимо приобрести этот навык, так как без него невозможна работа в команде, а также восприятие новых технологий, раскрываемых в основном посредством анализа содержательных (чужих) примеров (Samples). Чтобы обнаружить ошибку подобного рода, надо тщательно проанализировать код, в котором создается изображение (ветвь GL_QUAD_STRIP), и понять, что неправильно выбран индекс вершины. Замените строку givertex3f (xn, yn, zn); HaglVertexSf (xi, yi, zi); и вновь проверьте работу приложения. Зубчатость края должна исчезнуть, но в алгоритме, тем не менее, осталась еще небольшая, слабо заметная неточность. Ее обнаружение и исправление я оставляю вам, дорогой читатель.

Рис. 7.2. Вид поверхности при использовании режима GL_QUAD_STRIP

Обработку следующей команды меню мы проведем в том же стиле, за исключением того, что переменная m_FillMode не является булевской, хоть и принимает лишь два значения (GL_FILL и GL_LINE). Из материала предыдущей главы помните, возможен еще одни режим изображения полигонов — GL_POINT. Логику его реализации при желании вы введете самостоятельно, а сейчас введите коды двух функции обработки команды меню:

void COGView::OnViewFill(void)

{

//=== Переключаем режим заполнения четырехугольника

m_FillMode = m_FillMode==GL_FILL ? GL_LINE : GL__FILL;

//====== Заново создаем изображение

DrawScene();

Invalidate(FALSE);

UpdateWindow() ;

}

void COGView::OnUpdateViewFill(CCmdUI *pCmdUI)

{

//====== Вставляем или убираем маркер выбора

pCmdUI->SetCheck(m_FillMode==GL_FILL) ;

}

Запустите и проверьте работу команд меню. Отметьте, что формула учета освещения работает и в случае каркасного изображения примитивов (рис. 7.3).

Рис. 7.3. Вид поверхности, созданной в режиме GL_LINE

Для обмена с диалогом по управлению освещением нам понадобятся две вспомогательные функции GetLightParams и SetLightParam. Назначение первой из которых заполнить массив переменных, отражающих текущее состояние параметров освещения сцены OpenGL. Затем этот массив мы передадим в метод диалогового класса для синхронизации движков (sliders) управления. Вторая функция позволяет изменить отдельный параметр и привести его в соответствие с положением движка. Так как мы насчитали 11 параметров, которыми хотим управлять, то придется ввести в окно диалога 11 регуляторов, которым соответствует массив m_LightPaxam из 11 элементов. Массив уже помещен в класс COGView, нам осталось лишь задействовать его:

void COGView: :GetLightParams (int *pPos)

{

//====== Проход по всем регулировкам

for (int i=0; i<ll; i++)

//====== Заполняем транспортный массив pPos

pPos[i] = m_LightParam[i] ;

void COGView: :SetLightParam (short Ip, int nPos)

{ //====== Синхронизируем параметр lp и

//====== устанавливаем его в положение nPos

m_LightParam[lp] = nPos;

//=== Перерисовываем представление с учетом изменений

Invalidate (FALSE) ;

}

Диалог по управлению светом

В окне редактора диалогов (Resource View > Dialog > Контекстное меню > Insert Dialog) создайте окно диалога по управлению светом, которое должно иметь такой вид:

Рис. 7.4. Вид окна диалога по управлению параметрами света

Обратите внимание на то, что справа от каждого движка расположен элемент типа static Text, в окне которого будет отражено текущее положение движка в числовой форме. Три регулятора (элемента типа Slider Control) в левом верхнем углу окна диалога предназначены для управления свойствами света. Группа регуляторов справа от них поможет пользователю изменить координаты источника света. Группа регуляторов, объединенная рамкой (типа Group Box) с заголовком Material, служит для изменения отражающих свойств материала. Кнопка с надписью Data File позволит пользователю открыть файловый диалог и выбрать файл с данными для нового изображения. Для диалогов, предназначенных для работы в немодальном режиме, необходимо установить стиль Visible. Сделайте это в окне Properties > Behavior. Идентификаторы элементов управления мы сведем в табл. 7.1.

Таблица 7.1. Идентификаторы элементов управления

Элемент

Идентификатор

Диалог

IDD_PROP

Ползунок Ambient в группе Light

IDC_AMBIENT

Ползунок Diffuse в группе Light

IDC_DIFFUSE

Ползунок Specular в группе Light

IDC_SPECULAR

; Static Text справа от Ambient в группе Light

IDC_AMB_TEXT

, Static Text справа от Diffuse в группе Light

IDC_DIFFUSE_TEXT

Static Text справа от Specular в группе Light

IDC_SPECULAR_TEXT

Ползунок Ambient в группе Material

IDC_AMBMAT

Ползунок Diffuse в группе Material

IDC_DIFFMAT

' Ползунок Specular в группе Material

IDC_SPECMAT

f Static Text справа от Ambient в группе Material


IDC_AMBMAT_TEXT


:! Static Text справа от Diffuse. в группе Material

IDC_DIFFMATJFEXT

; Static Text справа от Specular в группе Material

IDC_SPECMAT_TEXT

Ползунок Shim'ness

IDC_SHINE

Ползунок Emission

IDC_EMISSION

« Static Text справа от Shininess

IDC_SHINE_TEXT

Static Text справа от Emission

IDC_EMISSION_TEXT

Ползунок X

IDC_XPOS

| Ползунок Y

IDC_YPOS

1 Ползунок Z

IDC_ZPOS

Static Text справа от X

IDC_XPOS_TEXT

Static Text справа от Y

IDC_YPOS_TEXT

Static Text справа от Z

IDC_ZPOS_TEXT

Кнопка Data File

IDC_FILENAME

 

Диалоговый класс

Для управления диалогом следует создать новый класс. Для этого можно воспользоваться контекстным меню, вызванным над формой диалога.

  1. Выберите в контекстном меню команду Add Class.
  2. В левом окне диалога Add Class раскройте дерево Visual C++, сделайте выбор MFC > MFC Class и нажмите кнопку Open.
  3. В окне мастера MFC Class Wizard задайте имя класса CPropDlg, в качестве базового класса выберите CDialog. При этом станет доступным ноле Dialog ID.
  4. В это поле введите или выберите из выпадающего списка идентификатор шаблона диалога IDD_PROP и нажмите кнопку Finish.

Просмотрите объявление класса CPropDlg, которое должно появиться в новом окне PropDlg.h. Как видите, мастер сделал заготовку функции DoDataExchange для обмена данными с элементами управления на форме диалога. Однако она нам не понадобится, так как обмен данными будет производиться в другом стиле, характерном для приложений не MFC-происхождения. Такое решение выбрано в связи с тем, что мы собираемся перенести рассматриваемый код в приложение, созданное на основе библиотеки шаблонов ATL. Это будет сделано в уроке 9 при разработке элемента ActiveX, а сейчас введите в диалоговый класс новые данные. Они необходимы для эффективной работы с диалогом в немодальном режиме. Важным моментом в таких случаях является использование указателя на оконный класс. С его помощью легко управлять окном прямо из диалога. Мы слегка изменили конструктор и ввели вспомогательный метод GetsiiderNum. Изменения косметического характера вы обнаружите сами:

#pragma once

class COGView; // Упреждающее объявление

class CPropDlg : public CDialog

{

DECLARE_DYNAMIC(CPropDlg)

public:

COGView *m_pView; // Адрес представления

int m_Pos[ll]; // Массив позиций ползунков

CPropDlg(COGView* p) ;

virtual ~CPropDlg();

// Метод для выяснения ID активного ползунка int GetsiiderNum(HWND hwnd, UINT& nID) ;

enum { IDD = IDD_PROP };

protected: virtual void DoDataExchange(CDataExchange* pDX);

DECLARE_MESSAGE_MAP()

};

Откройте файл реализации диалогового класса и с учетом сказанного про адрес окна введите изменение в тело конструктора, который должен приобрести такой вид:

CPropDlg::CPropDlg(COGView* p)

: CDialog(CPropDlg::IDD, p)

{

//====== Запоминаем адрес объекта

m_pView = p;

}

Инициализация диалога

При каждом открытии диалога все его элементы управления должны отражать текущие состояния регулировок (положения движков), которые хранятся в классе представления. Обычно эти установки производят в коде функции OninitDialog. Введите в класс CPropDlg стартовую заготовку этой функции (CPropDlg > Properties > Overrides > OninitDialog > Add) и наполните ее кодами, как показано ниже:

BOOL CPropDlg: rOnlnitDialog (void)

{ CDialog: :OnInitDialog () ;

//====== Заполняем массив текущих параметров света

m_pView->GetLightParams (m _Pos) ;

//====== Массив идентификаторов ползунков

UINT IDs[] =

{

IDC_XPOS, IDC_YPOS, IDC_ZPOS,

IDC_AMBIENT,

IDC_DIFFUSE,

IDC_SPECULAR,

IDC_AMBMAT,

IDC_DIFFMAT,

IDC_SPECMAT,

IDC_SHINE,

IDCEMISSION

//====== Цикл прохода по всем регуляторам

for (int i=0; Ksizeof (IDs) /sizeof (IDs [ 0] ) ; i++)

{

//=== Добываем Windows-описатель окна ползунка H

WND hwnd = GetDlgItem(IDs[i] } ->GetSafeHwnd () ;

UINT nID;

//====== Определяем его идентификатор

int num = GetSliderNum(hwnd, nID) ;

// Требуем установить ползунок в положение m_Pos[i]

: :SendMessage(hwnd, TBM_SETPOS, TRUE, (LPARAM) m_Pos [i] )

char s [ 8 ] ;

//====== Готовим текстовый аналог текущей позиции

sprintf (s, "%d" ,m_Pos [ i] ) ;

//====== Помещаем текст в окно справа от ползунка

SetDlgltemText (nID, (LPCTSTR) s) ;

}

return TRUE;

}

Вспомогательная функция GetsliderNum по переданному ей описателю окна (hwnd ползунка) определяет идентификатор связанного с ним информационного окна (типа Static text) и возвращает индекс соответствующей ползунку пози ции в массиве регуляторов:

int CPropDlg: :GetSliderNum (HWND hwnd, UINT& nID)

{

//==== GetDlgCtrllD по известному hwnd определяет

//==== и возвращает идентификатор элемента управления

switch ( : : GetDlgCtrllD (hwnd) )

{

// ====== Выясняем идентификатор окна справа

case IDC_XPOS:

nID = IDC_XPOS_TEXT;

return 0;

case IDC_YPOS:

nID = IDC_YPOS_TEXT;

return 1;

case IDC_ZPOS:

nID = IDC_ZPOS_TEXT;

return 2;

case IDC_AMBIENT:

nID = IDC_AMB_TEXT;

return 3;

case IDC_DIFFUSE:

nID = IDC_DIFFUSE_TEXT;

return 4 ;

case IDC_SPECULAR:

nID = IDC_SPECULAR_TEXT;

return 5; case IDC_AMBMAT:

nID = IDC_AMBMAT_TEXT;

return 6 ;

case IDC_DIFFMAT:

nID = IDC_DIFFMAT_TEXT;

return 7 ;

case IDC_SPECMAT:

nID = IDC_SPECMAT_TEXT;

return 8 ; case IDC_SHINE:

nID = IDC_SHINE_TEXT;

return 9;

case IDC_EMISSION:

nID = IDC_EMISSION_TEXT;

return 10;

}

return 0;

}

Работа с группой регуляторов

В диалоговый класс введите обработчики сообщений WM_HSCROLL и WM_CLOSE, a также реакцию на нажатие кнопки IDC_FILENAME. Воспользуйтесь для этого окном Properties и его кнопками Messages и Events. В обработчик OnHScroll введите логику определения ползунка и управления им с помощью мыши и клавиш. Подобный код мы подробно рассматривали в уроке 4. Прочтите объяснения вновь, если это необходимо, Вместе с сообщением WM_HSCROLL система прислала нам адрес объекта класса GScrollBar, связанного с активным ползунком. Мы добываем Windows-описатель его окна (hwnd) и передаем его в функцию GetsliderNum, которая возвращает целочисленный индекс. Последний используется для доступа к массиву позиций ползунков. Кроме этого, система передает nSBCode, который соответствует сообщению об одном из множества событий, которые могут произойти с ползунком (например, управление клавишей левой стрелки — SB_LINELEFT). В зависимости от события мы выбираем для ползунка новую позицию:

void CPropDlg::OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar)

{

//====== Windows-описатель окна активного ползунка

HWND hwnd = pScrollBar->GetSafeHwnd();

UINT nID;

//=== Определяем индекс в массиве позиций ползунков

int num = GetSliderNum(hwnd, nID) ;

int delta, newPos;

//====== Анализируем код события

switch (nSBCode)

{

case SBJTHUMBTRACK:

case SB_THUMBPOSITION: // Управление мышью

m_Pos[num] = nPos;

break; case SB_LEFT: // Клавиша Home

delta = -100;

goto New_Pos; case SB_RIGHT: // Клавиша End

delta = + 100;

goto New__Pos; case SB_LINELEFT: // Клавиша <-

delta = -1;

goto New_Pos; case SB_LINERIGHT: // Клавиша ->

delta = +1;

goto New_Pos; case SB_PAGELEFT: // Клавиша PgUp

delta = -20;

goto New_Pos; case SB_PAGERIGHT: // Клавиша PgDn

delta = +20-;

goto New_Pos;

New_Pos: // Общая ветвь

//====== Устанавливаем новое значение регулятора

newPos = m_Pos[num] + delta;

//====== Ограничения

m_Pos[num] = newPos<0 ? 0 :

newPos>100 ? 100 : newPos;

break; case SB ENDSCROLL:

default:

return;

}

//====== Синхронизируем текстовый аналог позиции

char s [ 8 ] ;

sprintf (s, "%d",m__Pos [num] ) ;

SetDlgltemText (nID, (LPCTSTR)s);

//---- Передаем изменение в класс COGView

m_pView->SetLightParam (num, m_Pos [num] ) ;

}

Особенности немодального режима

Рассматриваемый диалог используется в качестве панели управления освещением сцены, поэтому он должен работать в немодальном режиме. Особенностью такого режима, как вы знаете, является то, что при закрытии диалога он сам должен позаботиться об освобождении памяти, выделенной под объект собственного класса. Эту задачу можно решить разными способами. Здесь мы покажем, как это делается в функции обработки сообщения WM_CLOSE. До того как уничтожено Windows-окно диалога, мы обнуляем указатель m_pDlg, который должен храниться в классе COGView и содержать адрес объекта диалогового класса. Затем вызываем родительскую версию функции OnClose, которая уничтожает Windows-окно. Только после этого мы можем освободить память, занимаемую объектом своего класса:

void CPropDlg: :OnClose (void)

{

//=== Обнуляем указатель на объект своего класса

m_pView->m_pDlg = 0;

//====== Уничтожаем окно

CDialog: :OnClose () ;

//====== Освобождаем память

delete this;

}

Реакция на нажатие кнопки IDC_FILENAME совсем проста, так как основную работу выполняет класс COGView. Мы лишь вызываем функцию, которая реализована в этом классе:

void CPropDlg:: OnClickedFilename (void)

{

//=== Открываем файловый диалог и читаем данные

m_pView->ReadData ( ) ;

}

Создание немодального диалога должно происходить в ответ на выбор команды меню Edit > Properties. Обычно объект диалогового класса, используемого в немодальном режиме, создается динамически. При этом предполагается, что класс родительского окна хранит указатель m_pDlg на объект класса диалога. Значение указателя обычно используется не только для управления им, но и как признак его наличия в данный момент. Это позволяет правильно обработать ситуацию, когда диалог уже существует и вновь приходит команда о его открытии. Введите в класс COGView новую public-переменную:

CPropDlg *m_pDlg; // Указатель на объект диалога

В начало файла заголовков OGView.h вставьте упреждающее объявление класса

CPropDlg:

class CPropDlg; // Упреждающее объявление

В конструктор COGView вставьте обнуление указателя:

m_pDlg =0; // Диалог отсутствует

Для обеспечения видимости класса CPropDlg дополните список директив препроцессора файла OGView.cpp директивой:

linclude "PropDlg.h"

Теперь можно ввести коды функции, которая создает диалог и запускает его вызовом функции Create (в отличие от DoModal для модального режима). Если происходит попытка повторного открытия диалога, то возможны два варианта развития событий:

Реализуем первый вариант:

void COGView::OnEditProperties (void)

{

//====== Если диалог еще не открыт

if (!m_pDlg)

{

//=== Создаем его и запускаем в немодальном режиме

m_pDlg = new CPropDlg(this);

m_pDlg->Create(IDD_PROP);

}

else

// Иначе, переводим фокус в окно диалога

m_pDlg->SetActiveWindow();

}

Реакция на команду обновления пользовательского интерфейса при этом может быть такой:

void COGView::OnUpdateEditProperties(CCmdUI *pCmdUI)

{

pCmdUI->SetCheck (m_pDlg != 0);

}

Второй вариант потребует меньше усилий:

void COGView::OnEditProperties (void)

{

m_pDlg = new CPropDlg(this);

m_pDlg->Create(IDD_PROP); }

Но при этом необходима другая реакция на команду обновления интерфейса:

void COGView::OnUpdateEditProperties(CCmdUI *pCmdUI)

{

pCmdUI->Enable(m_pDlg == 0);

}

Выберите и реализуйте один из вариантов.

Панель управления

Завершая разработку приложения, вставьте в панель управления четыре кнопки

Для команд ID_EDIT_BACKGROUND, ID_EDIT_PROPERTIES, ID_VIEW_FILL И ID_VIEW_

QUAD. Заодно уберите из нее неиспользуемые нами кнопки с идентификаторами

ID_FILE_NEW, ID_FILE_OPEN, ID_FILE_SAVE, ID_FILE_PRINT, ID__EDIT_CUT,

ID_EDIT_COPY, ID_EDIT_PASTE. Запустите приложение, включите диалог Edit > Properties и попробуйте управлять регуляторами параметров света. Отметьте, что далеко не все из них отчетливым образом изменяют облик поверхности. Нажмите кнопку Data File, при этом должен открыться файловый диалог, но мы не сможем открыть никакого другого файла, кроме того, что был создан по умолчанию. Он имеет имя «Sin.dat» и должен находиться (и быть виден) в папке проекта. В качестве упражнения создайте какой-либо другой файл с данными, отражающими какую-либо поверхность в трехмерном пространстве. Вы можете воспользоваться для этой цели функцией DefaultGraphic, немного модифицировав ее код. На рис. 7.5 и 7.6 приведены поверхности, полученные таким способом. Вы можете видеть эффект, вносимый различными настройками параметров освещения.

Если вы тщательно протестируете поведение приложения, то обнаружите недостатки. Отметим один из них. Закрытые части изображения при некотором ракурсе просвечивают сквозь те части поверхности, которые находятся ближе к наблюдателю. Причину этого дефекта было достаточно трудно выявить. И здесь опять пришли на помощь молодые, талантливые слушатели Microsoft Authorized Educational Center (www.Avalon.ru) Кондрашов С. С. (scondor@rambler.ru) и Фролов Д. С. (dmfrolov@rambler.ru). Оказалось, что при задании типа проекции с помощью команды gluPerspective значения ближней границы фрустума не должны быть слишком маленькими:

gluPerspective (45., dAspect, 0.01, 10000.);

В нашем случае этот параметр равен 0.01. Замените его на 10. и сравните качество генерируемой поверхности.

Подведем итог. В этой главе мы:

Рис. 7.5. Вид поверхности, освещенной слева


Рис. 7.6. Вид той же поверхности, но освещенной справа